﻿using System;
using System.Collections.Generic;
using System.Windows;
using AzukiMap.MapSource;
using AzukiMap.Renderer;
using GeoAPI.Geometries;
using AzukiMap.Tools;
using System.Xml.Linq;

namespace AzukiMap.Layer
{
    public abstract class TiledLayer : MapLayer
    {
        private int _baseUriNum;
        protected List<MoveTile> _moveTileMethods;
        protected List<string> _renderedTiles;
        protected TileRenderer _renderer;

        protected TiledLayer()
        {
            Init();
        }

        protected abstract int TileWidth { get; }

        protected TiledLayer(TileSource mapSource, string name)
            : base(mapSource, name)
        {
            //IPolygon maxExtent = _geoUtil.EnvelopeToPolygon(mapSource.MaxExtent);
            TileSource = mapSource;
            MaxExtent = mapSource.MaxExtent;
            Init();
        }

        protected TiledLayer(TileSource mapSource, string name, IPolygon maxExtent)
            : base(mapSource, name)
        {
            TileSource = mapSource;
            MaxExtent = maxExtent;
            Init();
        }

        protected TiledLayer(TileSource mapSource, string name, IEnvelope maxExtent)
            : base(mapSource, name)
        {
            TileSource = mapSource;
            MaxExtent = _geoUtil.EnvelopeToPolygon(maxExtent);
            Init();
        }

        public TileSource TileSource
        {
            get { return (TileSource) base.MapSource; }
            protected set
            {
                if (value == null) throw new ArgumentNullException("value");
                base.MapSource = value;
            }
        }

        private void Init()
        {
            _renderer = new TileRenderer(this);
            _baseUriNum = 0;
            _moveTileMethods = new List<MoveTile>();
            _renderedTiles = new List<string>();
            SetTileMoveOrder();
        }

        public override void Show()
        {
            base.Show();
            Draw();
        }

        public override void Redraw()
        {
            base.Redraw();
            Draw();
        }

        public override void Draw()
        {
            base.Draw();
            if (!IsVisible)
            {
                return;
            }
            if (!CheckScale())
            {
                return;
            }
            // return when not drawable
            if (!IsDrawable)
            {
                return;
            } 
            double tileScale = GetTileScale(MapCoord.GeoScale);
            IPoint centerCoord = MapCoord.GeoCenter;
            ICoordinate tlPoint = GetTileOriginFromMapCenter(centerCoord, tileScale);
            var tileLeft = (int) tlPoint.X;
            var tileTop = (int) tlPoint.Y;

            bool isAllTilesDrawed = false;
            bool needsMoreTile = false;
            int tilesOnSameDirection = 1;

            int maxLeft = int.MaxValue;
            int minLeft = int.MinValue;
            int worldPixWidth = 0;
            bool needsToConnect = (Is180Connected && WestEnd != double.NegativeInfinity && EastEnd != double.PositiveInfinity);

            if ( needsToConnect)
            {
                var geomUtil = new GeometryUtil();
                double degreePerPix = 1 / GetPixelPerUnit(tileScale);
                
                var pointHalfTileFromStart = geomUtil.CreatePoint(WestEnd + degreePerPix * ((double)TileWidth / 2), 0);
                pointHalfTileFromStart.SRID = Srid;

                var pointHalfTileFromEnd = geomUtil.CreatePoint(EastEnd + degreePerPix * ((double)TileWidth / 2), 0);
                pointHalfTileFromEnd.SRID = Srid;

                minLeft = (int)Math.Floor(GetTileOriginFromMapCenter(pointHalfTileFromStart, tileScale).X);
                maxLeft = (int)Math.Floor(GetTileOriginFromMapCenter(pointHalfTileFromEnd, tileScale).X);
                //minLeft = (int)(MinX/degreePerPix);
                //maxLeft = (int) (MaxX/degreePerPix);

                worldPixWidth = maxLeft - minLeft;
            }

            // Draw tiles in a spiral from center tile.
            while (!isAllTilesDrawed)
            {
                isAllTilesDrawed = true;
                foreach (MoveTile moveTile in _moveTileMethods)
                {
                    for (int i = 0; i < tilesOnSameDirection; i++)
                    {
                        IPolygon tileExtent = GetTileExtent(tileScale, tileLeft, tileTop);

                        // Use tiles only intersects on a map canvas.
                        if (IsOnMapCanvas(tileExtent))
                        {
                            int reqLeft = tileLeft;
                            if ( needsToConnect )
                            {
                                if ( reqLeft < minLeft)
                                {
                                    reqLeft = reqLeft - (int)Math.Floor((double)(reqLeft - minLeft)/worldPixWidth)*worldPixWidth;
                                }
                                if ( maxLeft < reqLeft )
                                {
                                    reqLeft = reqLeft - (int)Math.Ceiling((double)(reqLeft - maxLeft)/worldPixWidth)*worldPixWidth;
                                }
                                if ( maxLeft == reqLeft)
                                {
                                    reqLeft -= worldPixWidth;
                                }
                            }
                            string tileReqStr = CreateTileReqString(tileScale, reqLeft, tileTop);
                            string renderedStr = tileReqStr + tileExtent.ToString();
                            if (!IsRendered(renderedStr))
                            {
                                TileData tile = CreateTile(tileReqStr, tileExtent, tileScale);
                                PlaceTileImages(_renderer.RenderTile(tile));
                                _renderedTiles.Add(renderedStr);
                            }
                            isAllTilesDrawed = false;
                        }
                        int[] tilePosition = moveTile(tileLeft, tileTop);
                        tileLeft = tilePosition[0];
                        tileTop = tilePosition[1];
                    }
                    // Two directions use same tile number.
                    if (needsMoreTile)
                    {
                        tilesOnSameDirection++;
                        needsMoreTile = false;
                    }
                    else
                    {
                        needsMoreTile = true;
                    }
                }
            }
        }

        public override void Clear()
        {
            base.Clear();

            Children.Clear();
            _renderedTiles.Clear();
        }

        private void SetTileMoveOrder()
        {
            _moveTileMethods.Add(MoveTileUp);
            _moveTileMethods.Add(MoveTileRight);
            _moveTileMethods.Add(MoveTileDown);
            _moveTileMethods.Add(MoveTileLeft);
        }

        protected bool IsOnMapCanvas(IPolygon tileExtent)
        {
            IPolygon extentProj = tileExtent;
            if (tileExtent.SRID != MapCoord.CurrentExtent.SRID)
            {
                extentProj = TransformUtil.Transform(tileExtent, MapCoord.CurrentExtent.SRID);
            }

            bool isInCanvas = (MapCoord.CurrentExtent.Intersects(extentProj));
            bool isInMaxExtent = true;
            if (MaxExtent != null)
            {
                IPolygon maxExtentProj = MaxExtent;
                if (maxExtentProj.SRID != MapCoord.CurrentExtent.SRID)
                {
                    maxExtentProj = TransformUtil.Transform(maxExtentProj, MapCoord.CurrentExtent.SRID);
                }
                isInMaxExtent = (maxExtentProj.Intersects(extentProj));
            }

            return (isInCanvas && isInMaxExtent);
        }

        protected string GetBaseUri()
        {
            string baseUri = TileSource.BaseUris[_baseUriNum];
            _baseUriNum++;
            if (_baseUriNum == TileSource.BaseUris.Count)
            {
                _baseUriNum = 0;
            }
            return baseUri;
        }

        protected virtual TileData CreateTile(string baseUri, string tileReqStr, IPolygon tileExtent, double tileScale)
        {
            var tileUri = new Uri(baseUri + tileReqStr, UriKind.RelativeOrAbsolute);

            var tile = new TileData(tileUri, tileExtent, TileSource.TileWidth,
                                    TileSource.TileHeight, TileSource.Srid, TileSource.Dpi, tileScale);
            return tile;
        }

        protected virtual TileData CreateTile(string tileReqStr, IPolygon tileExtent, double tileScale)
        {
            string baseUri = GetBaseUri();
            var tileUri = new Uri(baseUri + tileReqStr, UriKind.RelativeOrAbsolute);

            var tile = new TileData(tileUri, tileExtent, TileSource.TileWidth,
                                    TileSource.TileHeight, TileSource.Srid, TileSource.Dpi, tileScale);
            return tile;
        }

        protected virtual IPolygon GetTileExtent(double tileScale, int tlX, int tlY)
        {
            int blX = tlX;
            int blY = tlY + TileSource.TileHeight;
            int brX = blX + TileSource.TileWidth;
            int brY = blY;
            int trX = brX;
            int trY = tlY;

            IPoint tl = GetGeoPointFromTileCoord(tlX, tlY, tileScale);
            IPoint bl = GetGeoPointFromTileCoord(blX, blY, tileScale);
            IPoint br = GetGeoPointFromTileCoord(brX, brY, tileScale);
            IPoint tr = GetGeoPointFromTileCoord(trX, trY, tileScale);

            //return _geoUtil.CreateExtentPolygon(tl.X, tl.Y, bl.X, bl.Y, br.X, br.Y, tr.X, tr.Y);

            IPolygon tileExtent = _geoUtil.CreateExtentPolygon(tl.X, tl.Y, bl.X, bl.Y, br.X, br.Y, tr.X, tr.Y);
            tileExtent.SRID = tl.SRID;

            return tileExtent;
        }

        protected virtual double GetTileScale(double scale)
        {
            double nearestScale = 0;
            foreach (double tileScale in TileSource.Scales)
            {
                if (tileScale > scale)
                {
                    break;
                }
                nearestScale = tileScale;
            }

            if (nearestScale == 0)
            {
                nearestScale = TileSource.Scales[0];
            }
            return nearestScale;
        }

        protected void PlaceTileImages(IEnumerable<FrameworkElement> images)
        {
            foreach( var image in images )
            {
                if (RasterEffect != null)
                {
                    image.Effect = RasterEffect;
                }
                Children.Add(image);
            }
        }

        protected bool IsRendered(string tileUri)
        {
            bool isRendered = false;
            if (_renderedTiles.Contains(tileUri))
            {
                isRendered = true;
            }
            return isRendered;
        }

        protected virtual int[] MoveTileUp(int left, int top)
        {
            top = top - TileSource.TileHeight;
            return new int[2] {left, top};
        }

        protected virtual int[] MoveTileRight(int left, int top)
        {
            left = left + TileSource.TileWidth;
            return new int[2] {left, top};
        }

        protected virtual int[] MoveTileDown(int left, int top)
        {
            top = top + TileSource.TileHeight;
            return new int[2] {left, top};
        }

        protected virtual int[] MoveTileLeft(int left, int top)
        {
            left = left - TileSource.TileWidth;
            return new int[2] {left, top};
        }

        protected double GetPixelPerUnit(double tileScale)
        {
            int tileDpi = ((TileSource) MapSource).Dpi;
            double pixelPerUnit = MapSource.InchesPerUnit * tileDpi / tileScale;

            return pixelPerUnit;
        }

        public override XElement ToXml()
        {
            return ToXml("TiledLayer");
        }

        public override XElement ToXml(string elementName)
        {
            return base.ToXml(elementName);
        }

        protected abstract string CreateTileReqString(double tileScale, int tileLeft, int tileTop);
        protected abstract IPoint GetGeoPointFromTileCoord(int tx, int ty, double tileScale);
        protected abstract ICoordinate GetTileOriginFromMapCenter(IPoint point, double tileScale);

        #region Nested type: MoveTile

        protected delegate int[] MoveTile(int left, int top);

        #endregion
    }
}